package mh.map.render;

import static mh.entity.EnumState.BLASTED;
import static mh.entity.EnumState.BLASTING_OUT;
import static mh.entity.EnumState.FLOATING;
import static mh.entity.EnumState.LOCKED;
import static org.lwjgl.opengl.GL11.GL_QUADS;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glColor3f;
import static org.lwjgl.opengl.GL11.glColor4d;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glRecti;
import static org.lwjgl.opengl.GL11.glRotated;
import static org.lwjgl.opengl.GL11.glTexCoord2d;
import static org.lwjgl.opengl.GL11.glTranslated;
import static org.lwjgl.opengl.GL11.glVertex2d;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;

import mh.entity.Entity;
import mh.entity.EnumPosition;
import mh.entity.EnumState;
import mh.map.Texture;
import mh.map.TextureLoader;
import mh.util.Pair;

/**
 * This class represent all kind of drawable things other than <code>Tile</code>s.<br>
 * Its declare the basic method a render should have, like draw for example and also basic generic methods.
 * 
 * @author Gianmarco Laggia
 * @author Alberto D'Este
 */
public abstract class Render implements IDrawable {

	/** The texture this render refers to */
	protected Texture										texture;
	protected Entity										entity;
	/**
	 * Used in non OpenGL (LWJGL) environment.
	 */
	protected BufferedImage									image;
	protected final int										idFromDB;

	protected HashMap<EnumState, Rectangle>					statesPosition;
	protected HashMap<EnumState, Boolean>					statesMask;
	protected HashMap<EnumState, Pair<Integer, Integer>>	statesEnumAnimationData;
	protected HashMap<Rectangle, Pair<Integer, Integer>>	statesRectangleAnimationData;

	protected EnumPosition									registration;

	protected HashMap<EnumState, Long>						lastStepStates		= new HashMap<EnumState, Long>();
	/** The states of this entity with their actual frame position */
	protected HashMap<EnumState, Integer>					actualStatesFrames	= new HashMap<EnumState, Integer>();

	// ############### FLOATING
	private float											floatingOffset		= 0;
	// ############### END FLOATING

	// ############### BLASTING
	private long											lastStepBlast		= 0;

	private double[][][]									bActualOffsetPositions;
	private double[][]										bTransientValue;
	private double[][][]									bFinalOffsetPositions;
	private double[][][]									bRelativePos;
	private int[][]											bActualRot;
	private int[][]											bFinalRot;

	private static final int								P					= 6;
	private static final int								SPREAD				= 48;
	private static final int								BLASTING_FRAMES		= 10;
	private static final int								MAX_ROTATION		= 180;
	// ############### END BLASTING

	// ############### HEALTH BAR
	private long											HBLastStep			= 0;
	private static final int								HBDuration			= 3000;
	private static final int								HBFadeSpeed			= 35;
	private double											HBActualAlpha		= 0;

	// ############### END HEALTH BAR

	protected Render(BufferedImage img, int idFromDB, EnumPosition reg) {
		try {
			this.texture = TextureLoader.getTexture(img, false, null);
		} catch (RuntimeException e) {
			//			e.printStackTrace();
			System.out.println("No OpenGL context found in the current thread. I will work with AWT");
		} catch (IOException e) {
			e.printStackTrace();
		}
		this.image = img;
		this.idFromDB = idFromDB;
		this.statesPosition = new HashMap<EnumState, Rectangle>();
		this.statesMask = new HashMap<EnumState, Boolean>();
		this.statesEnumAnimationData = new HashMap<EnumState, Pair<Integer, Integer>>();
		this.statesRectangleAnimationData = new HashMap<Rectangle, Pair<Integer, Integer>>();
		this.registration = reg;
	}

	/**
	 * Copy Constructor, used mainly to cast from subclass to subclass, mantaining the same base fields.
	 * 
	 * @param r
	 */
	protected Render(Render r, Entity e) {
		this.image = r.image;
		this.texture = r.texture;
		this.idFromDB = r.idFromDB;
		this.statesEnumAnimationData = r.statesEnumAnimationData;
		this.statesRectangleAnimationData = r.statesRectangleAnimationData;
		this.statesPosition = r.statesPosition;
		this.statesMask = r.statesMask;
		this.registration = r.registration;
		this.lastStepStates = new HashMap<EnumState, Long>(r.lastStepStates); //should be copied and not re-referred
		this.actualStatesFrames = new HashMap<EnumState, Integer>(r.actualStatesFrames);
		this.actualStatesFrames.put(LOCKED, 0);
		this.actualStatesFrames.put(FLOATING, 0);
		this.entity = e;
	}

	/**
	 * Adds a new state data to this render
	 * 
	 * @param s
	 *            The kind of state
	 * @param r
	 *            The rectangle, bounds of the state
	 * @param frames
	 *            Number of frames of that state
	 * @param frameDuration
	 *            Duration of each frame
	 * @param isMask
	 *            true if the state is a mask
	 */
	public void addState(EnumState s, Rectangle r, int frames, int frameDuration, boolean isMask) {
		if (this.statesPosition.containsKey(s) && (r != null) && (frames > 0)) {
			this.statesPosition.remove(s);
		}
		Pair<Integer, Integer> p = new Pair<Integer, Integer>(frames, frameDuration);
		this.statesPosition.put(s, r);
		this.statesMask.put(s, isMask);
		this.statesEnumAnimationData.put(s, p);
		this.statesRectangleAnimationData.put(r, p);
		this.lastStepStates.put(s, 0L);
		this.actualStatesFrames.put(s, 0);
	}

	/**
	 * Draws the correct part of the texture, reading all the parameters from the related <code>Entity</code>
	 * 
	 * @param delta
	 *            Time passed since last tick
	 * @param posX
	 *            The X position where to draw the entity, in pixel.
	 * @param posY
	 *            The Y position where to draw the entity, in pixel.
	 */
	@Override
	public abstract void draw(long delta, double posX, double posY, boolean foreground);

	/**
	 * Draws the health-bar, with a fade-in and a fade-out animation when necessary
	 * 
	 * @param posX
	 *            Position of the object
	 * @param posY
	 *            Position of the object
	 * @param delta
	 *            Time passed since last tick
	 */
	protected void drawHealthBar(double posX, double posY, long delta) {
		if (this.entity.isHealthBarVisible()) {
			this.HBLastStep += delta;
			if ((this.HBActualAlpha < 1) && (this.HBLastStep < HBDuration)) {
				if (this.HBLastStep > HBFadeSpeed) {
					this.HBActualAlpha += 0.1;
					this.HBLastStep = 0;
				}
			} else {
				if (this.HBLastStep > HBDuration) {
					if (this.HBActualAlpha <= 0) {
						this.HBActualAlpha = 0;
						this.HBLastStep = 0;
						this.entity.setHealthBarVisibility(false);
					}
					long frame = this.HBLastStep - HBDuration;
					if (frame > HBFadeSpeed) {
						this.HBLastStep = HBDuration;
						this.HBActualAlpha -= 0.1;
					}
				} else {
					this.HBActualAlpha = 1;
				}
			}
			int actualHealth = (int) Math.round(this.entity.getHealthRatio(31));
			double percHealth = this.entity.getHealthRatio(100);
			glPushMatrix();
			glDisable(GL_TEXTURE_2D);
			glTranslated(posX, posY - 8, 0);
			glColor4d(0.1, 0.1, 0.1, this.HBActualAlpha);
			glRecti(0, 0, 32, 5);
			glColor4d(0.3, 0.3, 0.3, this.HBActualAlpha);
			glRecti(1, 1, 31, 4);
			if (percHealth > 70) {
				glColor4d(0, 1, 0, this.HBActualAlpha);
			} else if (percHealth > 20) {
				glColor4d(1, 1, 0, this.HBActualAlpha);
			} else {
				glColor4d(1, 0, 0, this.HBActualAlpha);
			}
			glRecti(1, 1, actualHealth + 1, 4);
			glEnable(GL_TEXTURE_2D);
			glPopMatrix();
		}
	}

	protected Pair<Double, Double> applyFloating(double posX, double posY, long delta) {
		if (this.entity.is(FLOATING)) {
			if (this.floatingOffset <= 1.9) {
				posY -= this.floatingOffset * 4;
			} else if ((this.floatingOffset > 1.9) && (this.floatingOffset <= 2)) {
				posY -= this.floatingOffset * 4;
				this.floatingOffset = (float) ((2 * Math.PI) + (3 * (Math.PI / 2)));
			} else {
				posY -= 8 + (Math.cos(this.floatingOffset) * 4);
			}
			long lastStep = 0;
			if (this.lastStepStates.get(FLOATING) != null) {
				lastStep = this.lastStepStates.get(FLOATING);
			}
			if (this.lastStepStates.get(FLOATING) == null) {
				lastStep = 0;
			}
			lastStep += delta;
			if (lastStep > 20) { //TIME OF FLOATING IS FIXED
				lastStep = 0;
				this.floatingOffset += 0.1;
			}
			this.lastStepStates.put(FLOATING, lastStep);
		} else {
			this.floatingOffset = 0;
		}
		return new Pair<Double, Double>(posX, posY);
	}

	protected void drawBlasted(EnumState s, double posX, double posY, long delta) {

		Rectangle textureOffset = this.statesPosition.get(s);
		int frames = this.statesRectangleAnimationData.get(textureOffset).getLeft();

		double verW = textureOffset.getWidth();
		double verH = textureOffset.getHeight();

		if (frames > 1) {
			verW = verW / frames;
		}

		// ###################### FLOAT ANIMATION
		Pair<Double, Double> pair = this.applyFloating(posX, posY, delta);
		posX = pair.getLeft();
		posY = pair.getRight();
		// ###################### END FLOAT ANIMATION

		double pieceW = verW / P;
		double pieceH = verH / P;

		if (this.bRelativePos == null) {
			this.bRelativePos = new double[P][P][4];
			for (int i = 0; i < P; i++) {
				for (int j = 0; j < P; j++) {
					double posXLeft = ((textureOffset.getX() + (j * pieceW)) * this.texture.getWidth()) / this.texture.getImageWidth();
					double posXRight = ((textureOffset.getX() + ((j + 1) * pieceW)) * this.texture.getWidth()) / this.texture.getImageWidth();
					double posYTop = ((textureOffset.getY() + (i * pieceH)) * this.texture.getHeight()) / this.texture.getImageHeight();
					double posYBottom = ((textureOffset.getY() + ((i + 1) * pieceH)) * this.texture.getHeight()) / this.texture.getImageHeight();
					this.bRelativePos[j][i][0] = posXLeft;
					this.bRelativePos[j][i][1] = posXRight;
					this.bRelativePos[j][i][2] = posYTop;
					this.bRelativePos[j][i][3] = posYBottom;
				}
			}
		}
		if ((this.bActualOffsetPositions == null) || (this.bActualRot == null)) {
			this.bActualOffsetPositions = new double[P][P][2];
			this.bActualRot = new int[P][P];
			for (int i = 0; i < P; i++) {
				for (int j = 0; j < P; j++) {
					this.bActualRot[j][i] = 0;
					this.bActualOffsetPositions[j][i][0] = (pieceW * j);
					this.bActualOffsetPositions[j][i][1] = (pieceH * i);
				}
			}
		}

		if (this.entity.is(BLASTING_OUT) && this.entity.is(BLASTED)) {
			int actualFrame = this.actualStatesFrames.get(BLASTING_OUT);//e.getFrame(BLASTING_OUT);
			if ((this.bFinalOffsetPositions == null) || (this.bFinalRot == null)) {
				this.bFinalOffsetPositions = new double[P][P][2];
				this.bTransientValue = new double[P][P];
				this.bFinalRot = new int[P][P];
				for (int i = 0; i < P; i++) {
					for (int j = 0; j < P; j++) {
						this.bFinalRot[j][i] = (int) Math.round(Math.random() * MAX_ROTATION);
						double randX;
						double randY;
						do {
							randX = Math.random() * SPREAD * 2;
							randY = Math.random() * SPREAD * 2;
						} while (((Math.pow(randX - SPREAD, 2) + Math.pow(randY - SPREAD, 2)) > Math.pow(SPREAD, 2)) || (randY < (SPREAD - 16)));
						this.bFinalOffsetPositions[j][i][0] = (randX - SPREAD) + 16;
						this.bFinalOffsetPositions[j][i][1] = (randY - SPREAD) + 16;//FIXME CONSTANT
						this.bTransientValue[j][i] = (this.bFinalRot[j][i] - this.bActualRot[j][i]) / BLASTING_FRAMES;
					}
				}
			}

			this.lastStepBlast += delta;
			if (this.lastStepBlast > 20) {
				this.lastStepBlast = 0;
				if ((actualFrame + 1) == BLASTING_FRAMES) {
					this.entity.getStates().remove(BLASTING_OUT);
					this.actualStatesFrames.put(BLASTING_OUT, 0);
					for (int i = 0; i < P; i++) {
						for (int j = 0; j < P; j++) {
							this.bActualOffsetPositions[j][i][0] = this.bFinalOffsetPositions[j][i][0];
							this.bActualOffsetPositions[j][i][1] = this.bFinalOffsetPositions[j][i][1];
							this.bActualRot[j][i] = this.bFinalRot[j][i];
						}
					}
					this.bFinalOffsetPositions = null;
				} else {
					for (int i = 0; i < P; i++) {
						for (int j = 0; j < P; j++) {
							this.bActualOffsetPositions[j][i][0] += (this.bFinalOffsetPositions[j][i][0] - this.bActualOffsetPositions[j][i][0]) / 2;//bTransientValue[j][i][0];
							this.bActualOffsetPositions[j][i][1] += (this.bFinalOffsetPositions[j][i][1] - this.bActualOffsetPositions[j][i][1]) / 2;// bTransientValue[j][i][1];
							this.bActualRot[j][i] += this.bTransientValue[j][i];
						}
					}
					this.actualStatesFrames.put(BLASTING_OUT, actualFrame + 1); //e.setFrame(BLASTING_OUT, actualFrame + 1);
				}
			}
		}
		for (int i = 0; i < P; i++) {
			for (int j = 0; j < P; j++) {

				glPushMatrix();
				this.texture.bind();
				glColor3f(1, 1, 1);
				glTranslated(posX + this.bActualOffsetPositions[j][i][0] + (pieceW / 2), posY + this.bActualOffsetPositions[j][i][1] + (pieceH / 2), 0);
				glRotated(this.bActualRot[j][i], 0, 0, 1);
				glTranslated(-pieceW / 2, -pieceH / 2, 0);
				glBegin(GL_QUADS);
				{
					glTexCoord2d(this.bRelativePos[j][i][0], this.bRelativePos[j][i][2]);
					glVertex2d(0, 0);
					glTexCoord2d(this.bRelativePos[j][i][0], this.bRelativePos[j][i][3]);
					glVertex2d(0, pieceH);
					glTexCoord2d(this.bRelativePos[j][i][1], this.bRelativePos[j][i][3]);
					glVertex2d(pieceW, pieceH);
					glTexCoord2d(this.bRelativePos[j][i][1], this.bRelativePos[j][i][2]);
					glVertex2d(pieceW, 0);
				}
				glEnd();
				glPopMatrix();
			}
		}
	}

	/**
	 * @return The univoke id used in the database
	 */
	public int getEntityDatabaseID() {
		return this.idFromDB;
	}

	/**
	 * @return Honestly, I really don't know what this method does
	 */
	public EnumState[] getRegisteredStates() {
		EnumState[] ret = new EnumState[this.statesPosition.size()];

		int i = 0;

		for (Entry<EnumState, Rectangle> e : this.statesPosition.entrySet()) {
			ret[i] = e.getKey();
			i++;
		}

		return ret;
	}

	/**
	 * Returns true if the specified state is to be considered as a mask
	 * 
	 * @param s
	 * @return boolean
	 */
	public boolean isMask(EnumState s) {
		if ((this.statesMask == null) || (this.statesMask.get(s) == null)) {
			return false;
		}
		return this.statesMask.get(s);
	}

}
